﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace Chat
{
    /// <summary>
    /// Args for event risen by the BaseEndPoint instanse upon abnormal termination
    /// of the command receiver thread (CmdReceiverThread class) owned by the 
    /// BaseEndPoint instanse.
    /// </summary>
    public class EndPointAbnormallyTerminatedEventArgs : EventArgs
    {
        public Exception ReasonException { get; protected set; }
        public EndPointAbnormallyTerminatedEventArgs(Exception reasonException)
        {
            ReasonException = reasonException;
        }
    }

    /// <summary>
    /// Modes for sending chat commands, used by BaseEndPoint class.
    /// </summary>
    public enum SendCommandMode
    {
        SendOnly = 0, // just send without handling exceptions 
        Throw = 1, // send, and if exception occurred upon send, re-throw SendCommandFailedException exception
        DisposeThrow = 2, // as Throw, but also dispose self, before the re-throwing 
    }

    /// <summary>
    /// The exception re-thrown by the BaseEndPoint instance, upon exception 
    /// occurred when sending chat command.
    /// </summary>
    public class SendCommandFailedException : ApplicationException
    {
        /// <summary>
        /// The chat command whose sending caused the exception
        /// </summary>
        public Command CommandToSend { get; protected set; }
        /// <summary>
        /// Whether the BaseEndPoint instanse was disposed (see SendCommandMode enum)
        /// </summary>
        public bool EndPointDisposed { get; protected set; }

        public SendCommandFailedException(
            Command commandToSend, bool endPointDisposed, Exception innerException) :
            base("Failed sending command.", innerException)
        {
            CommandToSend = commandToSend;
            EndPointDisposed = endPointDisposed;
        }
    }

    /// <summary>
    /// Abstraction for the chat client-server communication end point. When at server,
    /// will be used by the server to communicate with a paticular chat client
    /// connected to the server. When at client, will be used by the client to
    /// communicate with the chat server the client is connected to.
    /// Thus at server side there should be some kind of BaseEndPoint instanses
    /// collection, each used to communicate with a single particular chat client,
    /// connected to the server.
    /// 
    ///             THE SERVER SIDE                          =                THE CLIENT SIDE
    ///                                                      =
    ///    Collection of ServerEndPoint instanses            = 
    /// |------------------------------------------|         =              chat client 1     
    /// |                                          |         =          |------------------------------------------|
    /// |  ======================================  |         =          |  ======================================  |
    /// |  | ServerEndPoint :                   | communication channel |  | ClientEndPoint :                   |  |
    /// |  |     BaseEndPoint(Server.ServerCmd) +--------------------------+     BaseEndPoint(Client.ClientCmd) |  |
    /// |  ======================================  |         =          |  ======================================  |
    /// |                                          |         =          |------------------------------------------|
    /// |                                          |         =
    /// |                                          |         =              chat client 2      
    /// |                                          |         =          |------------------------------------------|
    /// |  ======================================  |         =          |  ======================================  |
    /// |  | ServerEndPoint :                   | communication channel |  | ClientEndPoint :                   |  |
    /// |  |     BaseEndPoint(Server.ServerCmd) +--------------------------+     BaseEndPoint(Client.ClientCmd) |  |
    /// |  ======================================  |         =          |  ======================================  |
    /// |                                          |         =          |------------------------------------------|
    /// |                                          |         =
    /// |                                          |         =
    /// |                                          |         =
    /// |     .................................    |         =              chat client N
    /// |                                          |         =          |------------------------------------------|
    /// |  ======================================  |         =          |  ======================================  |
    /// |  | ServerEndPoint :                   | communication channel |  | ClientEndPoint :                   |  |
    /// |  |     BaseEndPoint(Server.ServerCmd) +--------------------------+     BaseEndPoint(Client.ClientCmd) |  |
    /// |  ======================================  |         =          |  ======================================  |
    /// |                                          |         =          |------------------------------------------|
    /// |------------------------------------------|         =
    ///                                                      =
    /// The communication channel is represented by Connection(Server.ServerCmd) instanse at
    /// ServerEndPoint and by Connection(Client.ClientCmd) instanse at ClientEndPoint.
    /// </summary>
    /// <typeparam name="OutCmd"></typeparam>
    public abstract class BaseEndPoint<OutCmd> : IDisposable where OutCmd : Command
    {
        /// <summary>
        /// The communication channel between chat client&server
        /// </summary>
        protected Connection<OutCmd> Connection { get; set; }
        /// <summary>
        /// The serializer used to send chat commands via the communication channel.
        /// </summary>
        protected CmdSerializer<OutCmd> Serializer { get; set; }
        /// <summary>
        /// Receiver thread, running in the background and receiving chat commands.
        /// </summary>
        protected CmdReceiverThread CmdReceiverThread { get; set; }
        /// <summary>
        /// Whether the end point already disposed. Part of IDisposable design pattern.
        /// </summary>
        private bool disposed = false;
        /// <summary>
        /// The login data, for the chat user connected.
        /// </summary>
        public LoginData LoginData { get; protected set; }

        public BaseEndPoint(
            Connection<OutCmd> connection,
            CmdSerializer<OutCmd> serializer)
        {
            Connection = connection;
            Serializer = serializer;

            CmdReceiverThread = new CmdReceiverThread(
                Connection.GetCommandReceiver(Serializer));

            CmdReceiverThread.CommandReceived += new EventHandler<CmdReceivedEventArgs>(
                CmdReceiverThread_CommandReceived);

            CmdReceiverThread.Disconnected += new EventHandler<DisconnectedExEventArgs>(
                CmdReceiverThread_Disconnected);
        }
        /// <summary>
        /// Occurs, when the command receiver thread unexpectedly terminates.
        /// </summary>
        public event EventHandler<EndPointAbnormallyTerminatedEventArgs> AbnormallyTerminated;

        /// <summary>
        /// Starts the command receiver thread.
        /// </summary>
        public virtual void Start()
        {
            CmdReceiverThread.Start();
        }

        /// <summary>
        /// The static wrapper for BaseEndPoint.Start() method.
        /// Starts the specified end point.
        /// The added functionality: the BaseEndPoint instanse will be disposed
        /// upon the thread start fails.
        /// </summary>
        /// <param name="bep">The end point to start.</param>
        public static void Start(BaseEndPoint<OutCmd> bep)
        {
            try
            {
                bep.Start();
            }
            catch (Exception ex)
            {
                try
                {
                    bep.Dispose();
                }
                catch (Exception)
                {
                }

                throw ex;
            }
        }

        #region IDisposable Members
        /// <summary>
        /// IDisposable design pattern implementation.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        #endregion

        /// <summary>
        /// Sends the specified chat command in SendCommandMode.DisposeThrow mode.
        /// </summary>
        /// <param name="cmd">The chat command to send.</param>
        protected void SendCommand(OutCmd cmd)
        {
            SendCommand(cmd, SendCommandMode.DisposeThrow);
        }

        /// <summary>
        /// Sends the specifed chat command in the specified send mode.
        /// SendCommandMode.SendOnly : just send without handling exceptions.
        /// SendCommandMode.Throw: re-throws SendCommandFailedException upon any
        ///     exception caught while sending the specified command.
        /// SendCommandMode.DisposeThrow: as SendCommandMode.Throw,
        ///    but also disposes self, before the re-throwing
        /// </summary>
        /// <param name="cmd">The command to send.</param>
        /// <param name="sendMode">The mode for sending the command.</param>
        protected void SendCommand(OutCmd cmd, SendCommandMode sendMode)
        {
            switch (sendMode)
            {
                case SendCommandMode.SendOnly:
                    Connection.SendCmd(cmd, Serializer);
                    break;
                case SendCommandMode.Throw:
                case SendCommandMode.DisposeThrow:
                    try
                    {
                        Connection.SendCmd(cmd, Serializer);

                        // SystemTest(cmd);
                    }
                    catch (Exception ex)
                    {
                        bool dispose = sendMode == SendCommandMode.DisposeThrow;

                        if (dispose)
                            Dispose();

                        throw new SendCommandFailedException(cmd, dispose, ex);
                    }
                    break;
                default:
                    SendCommand(cmd, SendCommandMode.DisposeThrow);
                    break;
            }
        }

        /// <summary>
        /// Reads the chat command from the communication channel.
        /// </summary>
        /// <returns>The chat command received.</returns>
        protected object ReceiveCommand()
        {
            return Connection.ReceiveCmd(Serializer);
        }

        protected void TerminateAbnormally(Exception reasonException)
        {
            Dispose();
            OnAbnormallyTerminated(
                new EndPointAbnormallyTerminatedEventArgs(reasonException));
        }

        protected virtual void OnAbnormallyTerminated(EndPointAbnormallyTerminatedEventArgs e)
        {
            if (AbnormallyTerminated != null)
                AbnormallyTerminated(this, e);                
        }

        /// <summary>
        /// When overridden in derived classes on client and server sides,
        /// this method will process the specified command, which is received by
        /// the command receiver thread. This procession, of cource, will be different 
        /// at client and server sides. But the common thing is a deceision, whether 
        /// the specified command is valid in the context of the current state
        /// of the client or the server. If the command is invalid, false will be returned
        /// by the methid. See CmdReceiverThread_CommandReceived method of this class,
        /// to see how the return value of this absrtact method is used.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        /// <returns></returns>
        protected abstract bool ProcessReceivedCommand(object sender, CmdReceivedEventArgs e);

        protected virtual void Dispose(bool disposing)
        {
            if (!this.disposed)
            {
                disposed = true;

                if (disposing)
                {
                    CmdReceiverThread.StopGraceful();
                    Connection.Close();
                }
            }
        }

        ~BaseEndPoint()
        {
            Dispose(false);
        }

        void CmdReceiverThread_CommandReceived(object sender, CmdReceivedEventArgs e)
        {
            if (ProcessReceivedCommand(sender, e))
                return;

            e.Terminate = true;

            TerminateAbnormally(new UnknownCmdException(e.CommandReceived));
        }

        void CmdReceiverThread_Disconnected(object sender, DisconnectedExEventArgs e)
        {
            TerminateAbnormally(e.DisconnectedException);
        }

        private void SystemTest(OutCmd cmd)
        {
            ///
            // system tests
            ///
            if (cmd is Server.MessageCmd || cmd is Client.MessageCmd)
            {
                SystemTestMessageCmd(cmd);
            }
        }

        private void SystemTestMessageCmd(OutCmd cmd)
        {
            string message = string.Empty;

            if (cmd is Server.MessageCmd)
            {
                message = (cmd as Server.MessageCmd).MessageData.Message;
            }
            else
            {
                message = (cmd as Client.MessageCmd).MessageData.Message;
            }

            if (!message.StartsWith("@@SYSTEM_TEST,"))
                return;

            string[] testParams = message.Split(',');

            string testedSide = testParams[1];

            //
            // The condition below is the opposite (NOT) condition of the following,
            // which is the condition when we should perform the test:
            // (testSide == *) OR
            // ((testedSide == "client") AND (cmd is Client.ClientCmd)) OR
            // ((testedSide == "server") AND (cmd is Server.ServerCmd))
            if
            (
                (testedSide != "*") &&
                (testedSide != "client" || !(cmd is Client.ClientCmd)) &&
                (testedSide != "server" || !(cmd is Server.ServerCmd))
            )
            {
                return;
            }

            string tester = testParams[2]; // who sends a message

            if (tester != "*" && tester != LoginData.User.Value)
                return;

            string action = testParams[3];

            switch (action)
            {
                case "exception":
                    throw new ApplicationException("System test exception");
                case "stress":
                    int times = int.Parse(testParams[4]);
                    int timeout = int.Parse(testParams[5]);

                    SystemTestMessageCmdStress(cmd, times, timeout);

                    break;
                default:
                    break;
            }
        }

        private void SystemTestMessageCmdStress(OutCmd cmd, int times, int timeout)
        {
            for (int i = 0; i < times; i++)
            {
                Connection.SendCmd(cmd, Serializer);
                Thread.Sleep(timeout);
            }
        }
    }
}
